package com.ncmem.up6.store.minio;

import com.ncmem.up6.utils.Md5Tool;
import org.apache.commons.lang.StringUtils;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.*;

public class MinioAuthorization {

    private static final String String = null;
    private MinioConfig cfg;
    private String timeCur="";//20220519
    private String timeIso="";
    private String method="PUT";//PUT,GET,POST
    private String uri="";//资源路径,/test/test.txt
    private String host="";
    public String data_sha256_Hash="";//内容-sha256   HexEncode(Hash(RequestPayload))
    public String dataMd5="";
    public int contentLength=0;
    private Date timeNow= new Date();
    private String contentType="";
    private Map<String,String> m_headers=new HashMap<String,String>();
    private String CanonicalQueryString;//请求字符串

    /**
     * 构造
     */
    public MinioAuthorization() {

        this.timeIso = this.getTimeIso8601();
        DateFormat df = new SimpleDateFormat("yyyyMMdd");
        this.timeCur= df.format(this.timeNow);

        //header
        this.m_headers.put("content-length", "");
        this.m_headers.put("content-type", "");
        this.m_headers.put("host", "");
        this.m_headers.put("content-md5", "");
        this.m_headers.put("x-amz-content-sha256", "");
        this.m_headers.put("x-amz-date", "");
    }

    public MinioAuthorization setConfig(MinioConfig v) {
        this.cfg = v;
        return this;
    }

    private String getHeaders() {
        String head = this.CanonicalHeaders();
        String[] arr = head.split("\n");
        List<String> heads=new ArrayList<String>();

        for(String a : arr)
        {
            if(!a.isEmpty())
            {
                int end = a.length()-1;
                if(a.indexOf(":")!=-1)end = a.indexOf(":");
                heads.add(a.substring(0,end));
            }
        }
        head= StringUtils.join(heads.toArray(),";");
        //System.out.println("标头："+head);
        return head;
    }

    public MinioAuthorization setMethod(String v) {this.method = v;
        return this;}

    public MinioAuthorization setData(byte[] v) {
        this.contentLength = v.length;
        this.data_sha256_Hash = Md5Tool.base16( Md5Tool.sha256(v) );
        this.dataMd5 = Md5Tool.getMD5(v);
        this.dataMd5 = this.dataMd5.substring(8,24);
        try {
            this.dataMd5 = Base64.getEncoder().encodeToString(this.dataMd5.getBytes("utf-8"));
        } catch (UnsupportedEncodingException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return this;
    }
    public String getDataHash() {return this.data_sha256_Hash;}
    public MinioAuthorization setUrl(String v) {
        try {
            URL u = new URL(v);
            this.host = u.getHost();
            if( u.getPort() != 80 )
            {
                this.host += ":";
                this.host += u.getPort();
            }

            this.setUri(u.getPath());
            this.setQueryString(u.getQuery());
        } catch (MalformedURLException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return this;
    }

    /**
     * 参考：
     * https://docs.aws.amazon.com/zh_cn/AmazonS3/latest/API/sigv4-query-string-auth.html
     * URI encode every byte except the unreserved characters: 'A'-'Z', 'a'-'z', '0'-'9', '-', '.', '_', and '~'.
     * The space character is a reserved character and must be encoded as "%20" (and not as "+").
     * Each URI encoded byte is formed by a '%' and the two-digit hexadecimal value of the byte.
     * Letters in the hexadecimal value must be uppercase, for example "%1A".
     * Encode the forward slash character, '/', everywhere except in the object key name. For example, if the object key name is photos/Jan/sample.jpg, the forward slash in the key name is not encoded.
     * @param v
     * @return
     */
    public MinioAuthorization setUri(String v)
    {
        try {
            v = URLEncoder.encode(v,"UTF-8");
            v = v.replace("+", "%20");
            v = v.replace("*", "%2A");
            v = v.replace("%7E", "~");
            v = v.replace("%2F", "/");
            this.uri= v;
            //System.out.println("uri编码：\n"+this.uri);
        } catch (UnsupportedEncodingException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return this;
    }
    public MinioAuthorization setHost(String h) {this.host=h;
        return this;}
    public MinioAuthorization setContentType(String v) {this.contentType=v;
        return this;}
    public MinioAuthorization setQueryString(String v) {
        if(null==v ) return this;
        if( v.isEmpty()) return this;
        //?uploads -> uploads
        String[] arr = v.split("&");
        List<String> qs = new ArrayList<String>();
        for(String q : arr)
        {
            //所有参数后面必须跟=
            if(q.indexOf("=")==-1) q += "=";
            qs.add(q);
        }

        this.CanonicalQueryString = StringUtils.join(qs.toArray(), "&");
        return this;
    }

    public MinioAuthorization delHead(String n) {
        this.m_headers.remove(n.toLowerCase());
        return this;
    }

    public MinioAuthorization addHead(String n,String v) {
        this.m_headers.put(n, v);
        return this;
    }

    /**
     * 获取ISO时间
     * @return
     */
    private String getTimeIso8601() {
        DateFormat df = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'");
        df.setTimeZone(TimeZone.getTimeZone("UTC"));
        String t = df.format(this.timeNow);
        return t;
    }

    public String getTimeIso() {
        return this.timeIso;
    }

    byte[] HmacSHA256(String data, byte[] key) throws Exception
    {
        String algorithm="HmacSHA256";
        Mac mac = Mac.getInstance(algorithm);
        mac.init(new SecretKeySpec(key, algorithm));
        byte[] arr= mac.doFinal(data.getBytes("UTF-8"));
        //return Hex.encode(arr);
        return arr;
    }

    /**
     * 签名密钥
     * https://docs.aws.amazon.com/zh_cn/general/latest/gr/sigv4-calculate-signature.html
     * 	AWS4-HMAC-SHA256
     20150830T123600Z
     20150830/us-east-1/iam/aws4_request
     示例签名密钥
     f536975d06c0309214f805bb90ccff089219ecd68b2577efef23edd43b7e1a59
     * @return
     * @throws Exception
     */
    byte[] getSignatureKey() throws Exception
    {
        byte[] kSecret = ("AWS4" + this.cfg.sk).getBytes();
        byte[] kDate = HmacSHA256(this.timeCur, kSecret);
        byte[] kRegion = HmacSHA256(this.cfg.region, kDate);
        byte[] kService = HmacSHA256(this.cfg.service, kRegion);
        byte[] kSigning = HmacSHA256("aws4_request", kService);

        return kSigning;
    }

    /**
     * 规范标头
     * @return
     */
    String CanonicalHeaders() {
        String[] arr = {
                "content-length:"+this.contentLength,
                //"content-md5:"+this.dataMd5,
                "content-type:"+this.contentType,
                "host:"+this.host,
                "x-amz-content-sha256:"+this.data_sha256_Hash,
                //"x-amz-content-sha256:"+this.data_sha256_Hash,
                "x-amz-date:"+this.timeIso
        };

        //设置header值
        if(this.m_headers.containsKey("content-length"))
            this.m_headers.put("content-length",String.valueOf(this.contentLength) );
        if(this.m_headers.containsKey("content-type"))
            this.m_headers.put("content-type",this.contentType );
        if(this.m_headers.containsKey("host"))
            this.m_headers.put("host",this.host );
        if(this.m_headers.containsKey("content-md5"))
            this.m_headers.put("content-md5",this.dataMd5 );
        if(this.m_headers.containsKey("x-amz-content-sha256"))
            this.m_headers.put("x-amz-content-sha256",this.data_sha256_Hash );
        if(this.m_headers.containsKey("x-amz-date"))
            this.m_headers.put("x-amz-date",this.timeIso );

        List<String> hs = new ArrayList<String>();
        for(Object k : this.m_headers.keySet())
        {
            Object v = this.m_headers.get(k);
            hs.add(k+":"+v);
        }
        Collections.sort(hs);

        String str = StringUtils.join(hs.toArray(),"\n")+"\n";
        //System.out.println("规范标头："+str);
        return str;
    }

    /**
     * 凭证范围值，
     * https://docs.aws.amazon.com/zh_cn/general/latest/gr/sigv4-create-string-to-sign.html
     * 示例：
     * 	20150830/us-east-1/iam/aws4_request
     * @return
     */
    String CredentialScope() {
        String[] arr= {
                this.timeCur,
                this.cfg.region,
                "s3/aws4_request"
        };
        String str= StringUtils.join(arr,"/");
        //System.out.println("凭证范围：\n"+str);

        return str;
    }

    /**
     * 规范请求头签名
     * https://docs.aws.amazon.com/zh_cn/general/latest/gr/sigv4-create-canonical-request.html
     * 格式：
     * CanonicalRequest =
     HTTPRequestMethod + '\n' +
     CanonicalURI + '\n' +
     CanonicalQueryString + '\n' +
     CanonicalHeaders + '\n' +
     SignedHeaders + '\n' +
     HexEncode(Hash(RequestPayload))
     * @return
     */
    String HashCanonicalRequest() {
        String[] arr= {
                this.method,
                this.uri,
                this.CanonicalQueryString,//请求字符串
                this.CanonicalHeaders(),//请求头参数
                this.getHeaders(),//签名头
                this.data_sha256_Hash//内容-sha256
        };

        String str = StringUtils.join(arr, "\n");
        //System.out.println("规范请求头：\n"+str);
        str = Md5Tool.sha256(str);
        //System.out.println("规范请求头签名："+str);
        return str;
    }

    /**
     * 待签名字符串
     * https://docs.aws.amazon.com/zh_cn/general/latest/gr/sigv4-create-string-to-sign.html
     * 格式：
     * StringToSign =
     Algorithm + \n +
     RequestDateTime + \n +
     CredentialScope + \n +
     HashedCanonicalRequest
     * @return
     */
    private String StringSign() {
        String[] arr= {
                this.cfg.algorithm,
                this.timeIso,
                this.CredentialScope(),
                this.HashCanonicalRequest()
        };
        //待签名字符串
        String str = StringUtils.join(arr, "\n");
        try {

            //System.out.println("待签名字符串：\n"+str);
            //计算签名密钥
            byte[] key = this.getSignatureKey();
            {
                //System.out.println("签名密钥："+Md5Tool.base16(key) );
            }
            //签名
            str = Md5Tool.base16(  this.HmacSHA256(str, key));
            //System.out.println("密钥编码："+str);

        } catch (Exception e1) {
            // TODO Auto-generated catch block
            e1.printStackTrace();
        }
        return str;
    }

    /**
     * 待签名字符串
     * @return
     */
    public String Credential() {
        String[] arr= {
                this.cfg.ak,//
                this.timeCur,//时间：20220519
                this.cfg.region,//region
                this.cfg.service,//服务
                "aws4_request"
        };
        String v = StringUtils.join(arr, "/");
        //System.out.println("待签名字符串："+v);
        return v;
    }

    /**
     * Authorization 标头,添加到Header中
     * https://docs.aws.amazon.com/zh_cn/general/latest/gr/sigv4-add-signature-to-request.html
     * 格式：
     * 	Authorization: algorithm Credential=access key ID/credential scope, SignedHeaders=SignedHeaders, Signature=signature
     * 示例：
     * 	Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request, SignedHeaders=content-type;host;x-amz-date, Signature=5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7
     * @return
     */
    public String Authorization() {
        return this.cfg.algorithm + " " +
                "Credential=" + this.Credential() + ", "+
                "SignedHeaders="+this.getHeaders()+", "+//签名头
                "Signature=" + this.StringSign();
    }
}
